<html>
<head>
<script src="basis.js"></script>
<script src="renderer.js"></script>
<script src="dxt-to-rgb565.js"></script>
<script type="text/javascript">
function log(s) {
  var div = document.createElement('div');
  div.innerHTML = s;
  document.getElementById('logger').appendChild(div);
}

function logTime(desc, t) {
  log(t + 'ms ' + desc);
}

function isDef(v) {
  return typeof v != 'undefined';
}

function elem(id) {
  return document.getElementById(id);
}

formatTable = function(rows) {
  var colLengths = [];

  for (var i = 0; i < rows.length; i++) {
    var row = rows[i];
    for (var j = 0; j < row.length; j++) {
      if (colLengths.length <= j) colLengths.push(0);
      if (colLengths[j] < row[j].length) colLengths[j] = row[j].length;
    }
  }

  function formatRow(row) {
    var parts = [];
    for (var i = 0; i < colLengths.length; i++) {
      var s = row.length > i ? row[i] : '';
      var padding = (new Array(1 + colLengths[i] - s.length)).join(' ');
      if (s && s[0] >= '0' && s[0] <= '9') {
        // Right-align numbers.
        parts.push(padding + s);
      } else {
        parts.push(s + padding);
      }
    }
    return parts.join(' | ');
  }

  var width = 0;
  for (var i = 0; i < colLengths.length; i++) {
    width += colLengths[i];
    // Add another 3 for the separator.
    if (i != 0) width += 3;
  }

  var lines = [];
  lines.push(formatRow(rows[0]));
  lines.push((new Array(width + 1)).join('-'));
  for (var i = 1; i < rows.length; i++) {
    lines.push(formatRow(rows[i]));
  }

  return lines.join('\n');
};


function loadArrayBuffer(uri, callback) {
  log('Loading ' + uri + '...');
  var xhr = new XMLHttpRequest();
  xhr.responseType = "arraybuffer";
  xhr.open('GET', uri, true);
  xhr.onreadystatechange = function(e) {
    if (xhr.readyState == 4 && xhr.status == 200) {
      callback(xhr.response);
    }
  }
  xhr.send(null);
}

// DXT formats, from:
// http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
COMPRESSED_RGB_S3TC_DXT1_EXT  = 0x83F0;
COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;
COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;

BASIS_FORMAT = {
   
   cTFETC1: 0,
	cTFETC2: 1,
	cTFBC1: 2,
	cTFBC3: 3,
	cTFBC4: 4,
	cTFBC5: 5,		
	cTFBC7_M6_OPAQUE_ONLY: 6,
	cTFBC7_M5: 7,
	cTFPVRTC1_4_RGB: 8,
	cTFPVRTC1_4_RGBA: 9,
	
	cTFASTC_4x4: 10,
	
	cTFATC_RGB: 11,
	cTFATC_RGBA_INTERPOLATED_ALPHA: 12,
		
	cTFRGBA32: 13,
	cTFRGB565: 14,
	cTFBGR565: 15,
	cTFRGBA4444: 16
};

BASIS_DEBUG_FLAGS = {
	DEBUG_FLAG_VIS_CR: 1,
	DEBUG_FLAG_VIS_SELS: 2,
   	DEBUG_FLAG_VIS_ENDPOINTS: 4
};

BASIS_FORMAT_NAMES = {};
for (var name in BASIS_FORMAT) {
  BASIS_FORMAT_NAMES[BASIS_FORMAT[name]] = name;
}

DXT_FORMAT_MAP = {};
DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC1] = COMPRESSED_RGB_S3TC_DXT1_EXT;
DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC3] = COMPRESSED_RGBA_S3TC_DXT5_EXT;

var dxtSupported = true;
var drawMode = 0;

var basisHandle = 0, src = 0, srcSize = 0; 
var tex = 0, width = 0, height = 0, images = 0, levels = 0, have_alpha = false, alignedWidth = 0, alignedHeight = 0, format;
var curImageIndex = 0;

function redraw()
{
  if ((!width) || (!tex))
	return;
  
  if (dxtSupported) 
  {
	  renderer.drawTexture(tex, alignedWidth, alignedHeight, drawMode);
  }
  else if (format == BASIS_FORMAT.cTFBC1) 
  {
	  renderer.drawTexture(tex, alignedWidth, alignedHeight, drawMode);
  }
  else
  {
  	 log('TODO: Implement BC3->RGBA conversion for browsers that don\'t support BC3');
  }
}

function transcodeFrame()
{
  if (basisHandle == 0)
  	return;

  var dstSize = Module._basis_get_image_transcoded_size_in_bytes(basisHandle, 0, 0, format);
  var dst = Module._malloc(dstSize);

  if (!Module._basis_transcode_image(basisHandle, dst, dstSize, curImageIndex, 0, format, 1, 0))
  {
	  Module._basis_close(basisHandle); basisHandle = 0;
	  Module._free(src); src = 0;
	  Module._free(dst);
	  width = 0;

 	  log('basis_transcode_image() failed!');
 	  return;	
  }

  var dxtData = new Uint8Array(Module.HEAPU8.buffer, dst, dstSize);

  if (tex != 0)
  {
  	renderer.destroyTexture(tex);
  	tex = 0;
  }

  if (dxtSupported)
  {  
	  tex = renderer.createDxtTexture(dxtData, alignedWidth, alignedHeight, DXT_FORMAT_MAP[format]);  
  }
  else
  {
  	  var rgb565Data = dxtToRgb565(Module.HEAPU16, dst / 2, alignedWidth, alignedHeight);
	  
	  tex = renderer.createRgb565Texture(rgb565Data, alignedWidth, alignedHeight);
  }
  
  Module._free(dst);
}

var then = 0;
var timeSinceLastFrame = 0;
var g_framerateLimit = true;
var g_paused = false;
var g_Framerate = 24.0;
var g_targetFrame = 0.0;

function frame(now)
{
	now *= 0.001;  // convert to seconds
    const deltaTime = now - then;
    then = now;

	if (basisHandle != 0)
	{
		transcodeFrame();
 
  		redraw()
		
		if (g_targetFrame >= 0)
		{
			timeSinceLastFrame = 0;
			
			if (curImageIndex >= g_targetFrame)
				g_targetFrame = -1;
		    else
				curImageIndex++;
		}
		else if (g_paused)
		{
			timeSinceLastFrame = 0;
		}
		else if (!g_framerateLimit)
		{
			timeSinceLastFrame = 0;
			curImageIndex++;
		}
		else
		{
			timeSinceLastFrame += deltaTime;
			if (timeSinceLastFrame > 1.0/g_Framerate)
			{
				timeSinceLastFrame -= 1.0/g_Framerate;
				
				curImageIndex++;
			}
		}
		
		if (curImageIndex == images)
			curImageIndex = 0;
	}
	
	requestAnimationFrame(frame);
	
//	log('Here!');
}

function dataLoaded(data) 
{
  log('Done loading .basis file, decoded header:');
  
  Module._basis_init();

  if (basisHandle != 0)
  {
  	Module._basis_close(basisHandle);
  	basisHandle = 0;
  }
  
  if (src != 0)
  {
    src = 0;
  	Module._free(src);
  }
  
  srcSize = data.byteLength;
  var bytes = new Uint8Array(data);
  src = Module._malloc(srcSize);
  arrayBufferCopy(bytes, Module.HEAPU8, src, srcSize);
  
  basisHandle = Module._basis_open(src, srcSize);
  if (!basisHandle)
  {
	log('Failed opening basis file!');
	Module._free(src); src = 0;
	width = 0;
    return;  
  }

  width = Module._basis_get_image_width(basisHandle, 0, 0);
  height = Module._basis_get_image_height(basisHandle, 0, 0);
  images = Module._basis_get_num_images(basisHandle, 0);
  levels = Module._basis_get_num_levels(basisHandle, 0);
  has_alpha = Module._basis_get_has_alpha(basisHandle);
  
  if (!width || !height || !images || !levels)
  {
  	Module._basis_close(basisHandle); basisHandle = 0;
	Module._free(src); src = 0;
	width = 0;
	
	log('Failed getting info from basis file!');
    return;    
  }
    
  format = BASIS_FORMAT.cTFBC1;
  if (has_alpha)
  {
  	format = BASIS_FORMAT.cTFBC3;

	log('Decoding .basis data to BC3');
  }
  else
  {
  	log('Decoding .basis data to BC1');
  }

  log('.basis file size: ' + srcSize);
  if (levels == 1)
  	log('average bits/texel: ' + ((srcSize * 8.0) / (width * height * images)));
  log('width: ' + width);
  log('height: ' + height);
  log('images: ' + images);
  log('first image mipmap levels: ' + levels);
  log('has_alpha: ' + has_alpha);

  alignedWidth = (width + 3) & ~3;
  alignedHeight = (height + 3) & ~3;

  var canvas = elem('canvas');
  canvas.width = alignedWidth;
  canvas.height = alignedHeight;
  
  var status = Module._basis_start_transcoding(basisHandle);
  if (!status)
  {
	Module._basis_close(basisHandle); basisHandle = 0;
	Module._free(src); src = 0;
	width = 0;
	
    log('basis_start_transcoding() failed!');
    return;
  }
  
  curImageIndex = 0;
  
  //transcodeFrame();
 
  //redraw();
  
  //requestAnimationFrame(frame);
}

/**
 * @param {Uint8Array} src
 * @param {Uint8Array} dst
 */
function arrayBufferCopy(src, dst, dstByteOffset, numBytes) {
  dst32Offset = dstByteOffset / 4;
  var tail = (numBytes % 4);
  var src32 = new Uint32Array(src.buffer, 0, (numBytes - tail) / 4);
  var dst32 = new Uint32Array(dst.buffer);
  for (var i = 0; i < src32.length; i++) {
    dst32[dst32Offset + i] = src32[i];
  }
  for (var i = numBytes - tail; i < numBytes; i++) {
    dst[dstByteOffset + i] = src[i];
  }
}


function run() {
  elem('logger').innerHTML = '';
  loadArrayBuffer(elem('file').value, dataLoaded);
  
  requestAnimationFrame(frame);
}


function initialize() {
  var gl = elem('canvas').getContext('experimental-webgl');
  // Load the DXT extension, and verify it exists.
  if (!gl.getExtension('WEBKIT_WEBGL_compressed_texture_s3tc') && !gl.getExtension('WEBGL_compressed_texture_s3tc')) {
    dxtSupported = false;
    elem('nodxt').style.display = 'block';
  }

  window.renderer = new Renderer(gl);

  elem('file').addEventListener('keydown', function(e) {
    if (e.keyCode == 13) {
      run();
    }
  }, false);
    
  run();
}

function alphaBlend() { drawMode = 0; redraw(); }
function viewRGB() { drawMode = 1; redraw(); }
function viewAlpha() { drawMode = 2; redraw(); }

function visCR() 
{ 
    var i = Module._basis_get_debug_flags();
	i = i ^ BASIS_DEBUG_FLAGS.DEBUG_FLAG_VIS_CR;
	Module._basis_set_debug_flags(i);
}

function visSels() 
{ 
    var i = Module._basis_get_debug_flags();
	i = i ^ BASIS_DEBUG_FLAGS.DEBUG_FLAG_VIS_SELS;
	Module._basis_set_debug_flags(i);
}

function visEndpoints() 
{ 
    var i = Module._basis_get_debug_flags();
	i ^= BASIS_DEBUG_FLAGS.DEBUG_FLAG_VIS_ENDPOINTS;
	Module._basis_set_debug_flags(i);
}

function togglePause()
{
	g_paused = !g_paused;
}

function toggleFramerateLimit() 
{
	g_framerateLimit = !g_framerateLimit;
}

function restart()
{
	curImageIndex = 0;
}

function slow()
{
	if (g_Framerate >= 20.0)
		g_Framerate = 5.0;
	else
		g_Framerate = 20.0;
}

function rewind()
{
	if (g_targetFrame < 0)
	{	
		g_targetFrame = curImageIndex;
		if (g_targetFrame > 10)
			g_targetFrame -= 10;
		else
			g_targetFrame = 0;
		curImageIndex = 0;
	}
}

</script>
</head>
<body onload="initialize()">
  <br>
  <div style="font-size: 24pt; font-weight: bold">
    Basis Universal GPU Texture Video Transcoding Test (ETC1S->BC1)
  </div>

  <div style="font-size: 8pt;">
  <br>This demo uses the <a href="https://github.com/BinomialLLC/basis_universal">Basis C++ transcoder</a> (compiled to Javascript using Emscripten) to transcode a .basis Universal Texture Video file directly to BC1/BC3 texture data, with no intermediate recompression step.
  <br>.basis universal GPU texture files can be quickly transcoded directly to any other GPU texture format with little to no quality loss. For static texture content, .basis files are 10-25% smaller than rate distortion optimized (RDO) <a href="http://richg42.blogspot.com/2018/05/some-basis-baseline-universal-format.html">ETC1S</a> texture data losslessly compressed using LZMA. For video, we achieve bitrates that are significantly lower (average .5bpp vs. 2-3bpp) than I-Frame only (BC1+LZ4) Texture Video approaches such as <a href="https://hap.video/">HAP</a>.
  <br>Currently, video is a new use case for .basis files, so video encoding using the reference encoder is quite slow and can require a lot of memory (16-32GB) on larger videos, but it will be optimized over time.
  <br>Compared to regular pixel-based video, texture video doesn't utilize the IDCT and doesn't require colorspace conversion, chroma upsampling, SIMD, or deblocking filters.</br>
  <br>Thanks to Evan Parker for providing <a href="https://github.com/toji/webgl-texture-utils">webgl-texture-utils</a> and this test bed.
  </div>
  <br>
  <br>
      .basis file:
      <input id="file" type="text" size=30 value="kodim18.basis"></input>
      <input type="button" value="Run!" onclick="run()"></input>
  <br>
  <br>
      <input type="button" value="Alpha blend" onclick="alphaBlend()"></input>
      <input type="button" value="View RGB" onclick="viewRGB()"></input>
	  <input type="button" value="View Alpha" onclick="viewAlpha()"></input>
  <br>
  <br>	  
	  <input type="button" value="Visualize CR's" onclick="visCR()"></input>
	  <input type="button" value="Visualize Selectors" onclick="visSels()"></input>
	  <input type="button" value="Visualize Endpoints" onclick="visEndpoints()"></input>
  <br>
  <br>
	  <input type="button" value="No Framerate Limit" onclick="toggleFramerateLimit()"></input>
  	  <input type="button" value="Pause" onclick="togglePause()"></input>
	  <input type="button" value="Restart" onclick="restart()"></input>
  	  <input type="button" value="Toggle Slow Speed" onclick="slow()"></input>
      <input type="button" value="Rewind" onclick="rewind()"></input>
  
  <div style="position:absolute; left: 525px; top:130px; font-size: 20pt; font-weight: bold; color: red">
    <div id="nodxt" style="display: none; width: 768px; font-size: 20pt; font-weight: bold; color: red">
      NOTE: Your browser does not support DXT, so using RGB565
      for the texture below.  To get DXT, try Chrome 19+
      (beta channel as of 2012-04-20; graphics card DXT support also required).
    </div>
    <canvas id='canvas'></canvas>
  </div>
  <br><br>
  <div id='logger'></div>
</body>
</html>
